package NEHE.JOGL.Port;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.swing.*;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureCoords;
import com.sun.opengl.util.texture.TextureIO;
import static javax.media.opengl.GL.*;
import static java.awt.event.KeyEvent.*;

/**
 * NeHe Lesson 16: Cool Looking Fog
 */
public class NeheJOGL16Fog extends JPanel implements GLEventListener, KeyListener {
   private static final int REFRESH_FPS = 60;    // Display refresh frames per second
   private GLU glu;             // For the GL Utility
   final FPSAnimator animator;  // Used to drive display() 
   GLCanvas canvas;
   
   private static float rotateAngleX = 0; // rotational angle for x-axis in degree
   private static float rotateAngleY = 0; // rotational angle for y-axis in degree
   private static float z = -5.0f; // z-location
   private static float rotateSpeedX = 0.7f; // rotational speed for x-axis
   private static float rotateSpeedY = 0.9f; // rotational speed for y-axis

   private static float zIncrement = 0.5f; // for zoom in/out
   private static float rotateSpeedXIncrement = 0.5f; // adjusting x rotational speed
   private static float rotateSpeedYIncrement = 0.5f; // adjusting y rotational speed

   // Textures with three different filters - Nearest, Linear & MIPMAP
   private Texture[] textures = new Texture[3];
   private static int currTextureFilter = 0; // currently used filter
   private String textureFileName = "images/crate.png";

   int[] fogModes = { GL_EXP, GL_EXP2, GL_LINEAR }; // Storage For Three Types Of Fog
   int currFogFilter = 0; // Which Fog To Use
   float[] fogColor = { 0.5f, 0.5f, 0.5f, 1.0f }; // Fog Color

   // Texture image flips vertically. Shall use TextureCoords class to retrieve the
   // top, bottom, left and right coordinates.
   private float textureTop;
   private float textureBottom;
   private float textureLeft;
   private float textureRight;

   // Lighting
   private static boolean lightOn = true;
   // Ambient light does not come from a particular direction. Need some ambient
   // light to light up the scene. Ambient's value in RGBA
   private static float[] LightAmbient = { 0.5f, 0.5f, 0.5f, 1.0f };

   // Diffuse light comes from a particular location. Diffuse's value in RGBA
   private static float[] LightDiffuse = { 1.0f, 1.0f, 1.0f, 1.0f };
   // Diffuse light location xyz (in front of the screen).
   private static float LightDiffusePosition[] = { 0.0f, 0.0f, 2.0f, 1.0f };

   // Constructor
   public NeheJOGL16Fog() {
      canvas = new GLCanvas();
      this.setLayout(new BorderLayout());
      this.add(canvas, BorderLayout.CENTER);
      canvas.addGLEventListener(this);
      canvas.addKeyListener(this);
      canvas.setFocusable(true);  // To receive key event
      canvas.requestFocus();
   
      // Run the animation loop using the fixed-rate Frame-per-second animator,
      // which calls back display() at this fixed-rate (FPS).
      animator = new FPSAnimator(canvas, REFRESH_FPS, true);
   }

   // Main program
   public static void main(String[] args) {
      final int WINDOW_WIDTH = 320;
      final int WINDOW_HEIGHT = 240;
      final String WINDOW_TITLE = "Nehe #16: Cool Looking Fog";

      JFrame frame = new JFrame();
      final NeheJOGL16Fog joglMain = new NeheJOGL16Fog();
      frame.setContentPane(joglMain);
      frame.addWindowListener(new WindowAdapter() {
         @Override 
         public void windowClosing(WindowEvent e) {
            // Use a dedicate thread to run the stop() to ensure that the
            // animator stops before program exits.
            new Thread() {
               @Override 
               public void run() {
                  joglMain.animator.stop(); // stop the animator loop
                  System.exit(0);
               }
            }.start();
         }
      });
      frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
      frame.setTitle(WINDOW_TITLE);
      frame.setVisible(true);
      joglMain.animator.start(); // start the animation loop
   }

   // ------ Implement methods declared in GLEventListener ------

   /**
    * Called by the drawable immediately after the OpenGL context is initialized for
    * the first time. Can be used to perform one-time OpenGL initialization such as
    * setup of lights and display lists. Run only once.
    */
   @Override
   public void init(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // GL Utilities
      glu = new GLU();
      // Enable smooth shading, which blends colors nicely across a polygon, and
      // smoothes out lighting.
      gl.glShadeModel(GL_SMOOTH);
      // Set background color (in RGBA). Alpha of 0 for total transparency
      //gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
      // Setup the depth buffer & enable the depth testing
      gl.glClearDepth(1.0f);
      gl.glEnable(GL_DEPTH_TEST); // enables depth testing
      gl.glDepthFunc(GL_LEQUAL); // the type of depth test to do
      // We want the best perspective correction to be done
      gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

      gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); // We'll Clear To The Color Of The Fog
                                               // ( Modified )

      // Set up fog mode
      gl.glFogfv(GL_FOG_COLOR, fogColor, 0); // Set Fog Color
      gl.glFogf(GL_FOG_DENSITY, 0.35f); // How Dense Will The Fog Be
      gl.glHint(GL_FOG_HINT, GL_DONT_CARE); // Fog Hint Value
      gl.glFogf(GL_FOG_START, 1.0f); // Fog Start Depth
      gl.glFogf(GL_FOG_END, 5.0f); // Fog End Depth
      gl.glEnable(GL_FOG); // Enables GL_FOG

      // Load the texture image
      try {
         // Use URL so that can read from JAR and disk file.
         BufferedImage image = ImageIO.read(this.getClass().getResource(
               textureFileName));

         // Create a OpenGL Texture object from (URL, mipmap, file suffix)
         textures[0] = TextureIO.newTexture(image, false);
         // Nearest filter is least compute-intensive
         // Use nearer filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
         // Use nearer filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

         textures[1] = TextureIO.newTexture(image, false);
         // Linear filter is more compute-intensive
         // Use linear filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         // Use linear filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

         textures[2] = TextureIO.newTexture(image, true); // mipmap is true
         // Use mipmap filter is the image is smaller than the texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
               GL_LINEAR_MIPMAP_NEAREST);
      } catch (GLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      // Get the top and bottom coordinates of the textures. Image flips vertically.
      TextureCoords textureCoords;
      textureCoords = textures[0].getImageTexCoords();
      textureTop = textureCoords.top();
      textureBottom = textureCoords.bottom();
      textureLeft = textureCoords.left();
      textureRight = textureCoords.right();

      // Enable the texture
      gl.glEnable(GL_TEXTURE_2D);
      // texture.enable();

      // Set up the lighting for light named GL_LIGHT1
      gl.glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient, 0);
      gl.glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse, 0);
      gl.glLightfv(GL_LIGHT1, GL_POSITION, LightDiffusePosition, 0);
      gl.glEnable(GL_LIGHT1);
   }

   /**
    * Called by the drawable to initiate OpenGL rendering.
    */
   @Override
   public void display(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // Clear the screen and the depth buffer
      gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      // Reset the view (x, y, z axes back to normal)
      gl.glLoadIdentity();

      // set up fog mode
      gl.glFogi(GL_FOG_MODE, fogModes[currFogFilter]); // Fog Mode

      // Check whether light shall be turn on (toggle via the 'L' key)
      // This LIGHTING is different form LIGHT1 (??)
      if (lightOn) {
         gl.glEnable(GL_LIGHTING);
      } else {
         gl.glDisable(GL_LIGHTING);
      }

      // ------ Render a Cube with texture ------

      gl.glTranslatef(0.0f, 0.0f, z); // translate into the screen
      gl.glRotatef(rotateAngleX, 1.0f, 0.0f, 0.0f); // rotate about the x-axis
      gl.glRotatef(rotateAngleY, 0.0f, 1.0f, 0.0f); // rotate about the y-axis

      // Bind the texture with the currently chosen filter to the current OpenGL
      // graphics context.
      textures[currTextureFilter].bind();

      gl.glBegin(GL_QUADS); // of the color cube
      // Define groups of 4 vertices in CCW order

      // Front Face
      gl.glNormal3f(0.0f, 0.0f, 1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, 1.0f); // bottom-left of the texture and quad
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, 1.0f); // bottom-right of the texture and quad
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(1.0f, 1.0f, 1.0f); // top-right of the texture and quad
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, 1.0f); // top-left of the texture and quad
      // Back Face
      gl.glNormal3f(0.0f, 0.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, -1.0f);
      // Top Face
      gl.glNormal3f(0.0f, 1.0f, 0.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(-1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(1.0f, 1.0f, -1.0f);
      // Bottom Face
      gl.glNormal3f(0.0f, -1.0f, 0.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(-1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, 1.0f);
      // Right face
      gl.glNormal3f(1.0f, 0.0f, 0.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(1.0f, 1.0f, -1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(1.0f, -1.0f, 1.0f);
      // Left Face
      gl.glNormal3f(-1.0f, 0.0f, 0.0f);
      gl.glTexCoord2f(textureLeft, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, -1.0f);
      gl.glTexCoord2f(textureRight, textureBottom);
      gl.glVertex3f(-1.0f, -1.0f, 1.0f);
      gl.glTexCoord2f(textureRight, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, 1.0f);
      gl.glTexCoord2f(textureLeft, textureTop);
      gl.glVertex3f(-1.0f, 1.0f, -1.0f);

      gl.glEnd();

      // Update the rotational position after each refresh, based on rotational
      // speed.
      rotateAngleX += rotateSpeedX;
      rotateAngleY += rotateSpeedY;
   }

   /**
    * Called by the drawable during the first repaint after the component has been
    * resized. Run at least once when the window is first set to visible.
    */
   @Override
   public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();

      if (height == 0) {
         height = 1; // prevent divide by zero
      }
      float aspect = (float)width / height;

      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);

      // Enable perspective view (where far objects are smaller)
      gl.glMatrixMode(GL_PROJECTION);
      gl.glLoadIdentity(); // reset
      // View angle of 45 degrees, based on this canvas's aspect ratio. The 0.1f,
      // 100.0f is the starting and ending points for how deep we can draw into
      // the screen.
      glu.gluPerspective(45.0f, aspect, 0.1f, 100.0f); // fovy, aspect, zNear, zFar

      // Enable the model view - any new transformations will affect the model-view
      // matrix
      gl.glMatrixMode(GL_MODELVIEW);
      gl.glLoadIdentity(); // reset
   }

   /**
    * Called when the display mode (eg. resolution) has been changed.
    */
   @Override
   public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
         boolean deviceChanged) {}

   // ----- Implement methods declared in KeyListener -----

   @Override
   public void keyPressed(KeyEvent e) {
      switch (e.getKeyCode()) {
         case VK_L: // toggle light on/off
            lightOn = !lightOn;
            break;
         case VK_F: // switch to the next filter (NEAREST, LINEAR, MIPMAP)
            currTextureFilter = (currTextureFilter + 1) % textures.length;
            break;
         case VK_PAGE_UP: // zoom-out
            z -= zIncrement;
            break;
         case VK_PAGE_DOWN: // zoom-in
            z += zIncrement;
            break;
         case VK_UP: // decrease rotational speed in x
            rotateSpeedX -= rotateSpeedXIncrement;
            break;
         case VK_DOWN: // increase rotational speed in x
            rotateSpeedX += rotateSpeedXIncrement;
            break;
         case VK_LEFT: // decrease rotational speed in y
            rotateSpeedY -= rotateSpeedYIncrement;
            break;
         case VK_RIGHT: // increase rotational speed in y
            rotateSpeedY += rotateSpeedYIncrement;
            break;
         case VK_G: // switch to the next fog mode
            currFogFilter = (currFogFilter + 1) % fogModes.length;
            break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {}

   @Override
   public void keyTyped(KeyEvent e) {}
}
